From 0c25226b35447a51918d5a17ba62a36b109757fe Mon Sep 17 00:00:00 2001 From: Jakub Bukaj Date: Sat, 22 Nov 2014 09:36:14 -0500 Subject: [PATCH] Add a `search` command to cargo --- src/bin/cargo.rs | 1 + src/bin/search.rs | 35 +++++++++++++++++++++++++ src/cargo/ops/mod.rs | 2 +- src/cargo/ops/registry.rs | 50 ++++++++++++++++++++++++++++++++--- src/registry/lib.rs | 55 +++++++++++++++++++++++++++++---------- 5 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 src/bin/search.rs diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index baeb90db3..40d459b9b 100644 --- a/src/bin/cargo.rs +++ b/src/bin/cargo.rs @@ -74,6 +74,7 @@ macro_rules! each_subcommand( ($macro:ident) => ({ $macro!(publish) $macro!(read_manifest) $macro!(run) + $macro!(search) $macro!(test) $macro!(update) $macro!(verify_project) diff --git a/src/bin/search.rs b/src/bin/search.rs new file mode 100644 index 000000000..95d01bc30 --- /dev/null +++ b/src/bin/search.rs @@ -0,0 +1,35 @@ +use cargo::ops; +use cargo::core::{MultiShell}; +use cargo::util::{CliResult, CliError}; + +#[deriving(Decodable)] +struct Options { + flag_host: Option, + flag_verbose: bool, + arg_query: String +} + +pub const USAGE: &'static str = " +Search packages in crates.io + +Usage: + cargo search [options] + +Options: + -h, --help Print this message + --host HOST Host of a registry to search in + -v, --verbose Use verbose output +"; + +pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult> { + shell.set_verbose(options.flag_verbose); + let Options { + flag_host: host, + arg_query: query, + .. + } = options; + + ops::search(query.as_slice(), shell, host) + .map(|_| None) + .map_err(|err| CliError::from_boxed(err, 101)) +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index a23bd7dc2..53ade356e 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -16,7 +16,7 @@ pub use self::lockfile::{write_lockfile, write_pkg_lockfile}; pub use self::cargo_test::{run_tests, run_benches, TestOptions}; pub use self::cargo_package::package; pub use self::registry::{publish, registry_configuration, RegistryConfig}; -pub use self::registry::{registry_login, http_proxy, http_handle}; +pub use self::registry::{registry_login, search, http_proxy, http_handle}; pub use self::registry::{modify_owners, yank, OwnersOptions}; pub use self::cargo_fetch::{fetch}; pub use self::cargo_pkgid::pkgid; diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index 1486771ea..c4fbeafae 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::io::File; use std::os; +use term::color::BLACK; use curl::http; use git2; @@ -12,7 +13,7 @@ use core::manifest::ManifestMetadata; use ops; use sources::{PathSource, RegistrySource}; use util::config; -use util::{CargoResult, human, internal, ChainError, Require, ToUrl}; +use util::{CargoResult, human, internal, ChainError, ToUrl}; use util::config::{Config, ConfigValue, Location}; pub struct RegistryConfig { @@ -143,9 +144,7 @@ pub fn registry(shell: &mut MultiShell, token: token_config, index: index_config, } = try!(registry_configuration()); - let token = try!(token.or(token_config).require(|| { - human("no upload token found, please run `cargo login`") - })); + let token = token.or(token_config); let index = index.or(index_config).unwrap_or(RegistrySource::default_url()); let index = try!(index.as_slice().to_url().map_err(human)); let sid = SourceId::for_registry(&index); @@ -323,3 +322,46 @@ pub fn yank(manifest_path: &Path, Ok(()) } + +pub fn search(query: &str, shell: &mut MultiShell, index: Option) -> CargoResult<()> { + fn truncate_with_ellipsis(s: &str, max_length: uint) -> String { + if s.len() < max_length { + s.to_string() + } else { + format!("{}…", s[..max_length - 1]) + } + } + + let (mut registry, _) = try!(registry(shell, None, index)); + + let crates = try!(registry.search(query).map_err(|e| { + human(format!("failed to retrieve search results from the registry: {}", e)) + })); + + let list_items = crates.iter() + .map(|krate| ( + format!("{} ({})", krate.name, krate.max_version), + krate.description.as_ref().map(|desc| + truncate_with_ellipsis(desc.replace("\n", " ").as_slice(), 128)) + )) + .collect::>(); + let description_margin = list_items.iter() + .map(|&(ref left, _)| left.len() + 4) + .max() + .unwrap_or(0); + + for (name, description) in list_items.into_iter() { + let line = match description { + Some(desc) => { + let space = String::from_char( + description_margin - name.len(), + ' '); + name + space + desc + } + None => name + }; + try!(shell.say(line, BLACK)); + } + + Ok(()) +} diff --git a/src/registry/lib.rs b/src/registry/lib.rs index 8768c592a..0e5be48fe 100644 --- a/src/registry/lib.rs +++ b/src/registry/lib.rs @@ -13,21 +13,35 @@ use serialize::json; pub struct Registry { host: String, - token: String, + token: Option, handle: http::Handle, } pub type Result = result::Result; +#[deriving(PartialEq)] +pub enum Auth { + Authorized, + Unauthorized +} + pub enum Error { Curl(curl::ErrCode), NotOkResponse(http::Response), NonUtf8Body, Api(Vec), Unauthorized, + TokenMissing, Io(io::IoError), } +#[deriving(Decodable)] +pub struct Crate { + pub name: String, + pub description: Option, + pub max_version: String +} + #[deriving(Encodable)] pub struct NewCrate { pub name: String, @@ -68,13 +82,14 @@ pub struct User { #[deriving(Decodable)] struct ApiError { detail: String } #[deriving(Encodable)] struct OwnersReq<'a> { users: &'a [&'a str] } #[deriving(Decodable)] struct Users { users: Vec } +#[deriving(Decodable)] struct Crates { crates: Vec } impl Registry { - pub fn new(host: String, token: String) -> Registry { + pub fn new(host: String, token: Option) -> Registry { Registry::new_handle(host, token, http::Handle::new()) } - pub fn new_handle(host: String, token: String, + pub fn new_handle(host: String, token: Option, handle: http::Handle) -> Registry { Registry { host: host, @@ -126,16 +141,23 @@ impl Registry { box tarball as Box].into_iter()); let url = format!("{}/api/v1/crates/new", self.host); - let response = handle(self.handle.put(url, &mut body) - .content_length(size) - .header("Authorization", - self.token.as_slice()) - .header("Accept", "application/json") - .exec()); + + let token = try!(self.token.as_ref().ok_or(Error::TokenMissing)).as_slice(); + let request = self.handle.put(url, &mut body) + .content_length(size) + .header("Accept", "application/json") + .header("Authorization", token); + let response = handle(request.exec()); let _body = try!(response); Ok(()) } + pub fn search(&mut self, query: &str) -> Result> { + let body = try!(self.req(format!("/crates?q={}", query), None, Get, Auth::Unauthorized)); + + Ok(json::decode::(body.as_slice()).unwrap().crates) + } + pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> { let body = try!(self.delete(format!("/crates/{}/{}/yank", krate, version), None)); @@ -151,24 +173,28 @@ impl Registry { } fn put(&mut self, path: String, b: &[u8]) -> Result { - self.req(path, Some(b), Put) + self.req(path, Some(b), Put, Auth::Authorized) } fn get(&mut self, path: String) -> Result { - self.req(path, None, Get) + self.req(path, None, Get, Auth::Authorized) } fn delete(&mut self, path: String, b: Option<&[u8]>) -> Result { - self.req(path, b, Delete) + self.req(path, b, Delete, Auth::Authorized) } fn req(&mut self, path: String, body: Option<&[u8]>, - method: Method) -> Result { + method: Method, authorized: Auth) -> Result { let mut req = Request::new(&mut self.handle, method) .uri(format!("{}/api/v1{}", self.host, path)) - .header("Authorization", self.token.as_slice()) .header("Accept", "application/json") .content_type("application/json"); + + let token = try!(self.token.as_ref().ok_or(Error::TokenMissing)).as_slice(); + if authorized == Auth::Authorized { + req = req.header("Authorization", token); + } match body { Some(b) => req = req.body(b), None => {} @@ -213,6 +239,7 @@ impl fmt::Show for Error { write!(f, "api errors: {}", errs.connect(", ")) } Error::Unauthorized => write!(f, "unauthorized API access"), + Error::TokenMissing => write!(f, "no upload token found, please run `cargo login`"), Error::Io(ref e) => write!(f, "io error: {}", e), } } -- 2.30.2